// posix header
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>

// openMP header
#include <hdf5.h>
#include <mpi.h>
#include <omp.h>

// c++ header
#include <algorithm>
#include <fstream>
#include <iostream>
#include <regex>
#include <sstream>
#include <stdexcept>
#include <thread>
#include <vector>

using namespace std;

int mpi_rank, mpi_size;

std::string one_dot(".");
std::string tow_dot("..");

template <typename T>
hid_t MPI_Type() {
  std::exit(1);
  return 0;
}

template <>
hid_t MPI_Type<double>() {
  return H5T_NATIVE_DOUBLE;
}

template <>
hid_t MPI_Type<long>() {
  return H5T_NATIVE_LONG;
}

template <typename T, int N_dims>
void Hdf5Create(const std::string& file_path, const std::string& dataset_name,
                hsize_t (&dims)[N_dims]) {
  // make sure the file exists
  H5Fcreate(file_path.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);

  // attempt to open the file
  hid_t file_id = H5Fopen(file_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
  if (file_id < 0) {
    throw std::invalid_argument("Could not open " + file_path);
  }

  // create the dataset
  hid_t space_id = H5Screate_simple(N_dims, dims, nullptr);
  H5Dcreate1(file_id, dataset_name.c_str(), MPI_Type<T>(), space_id,
             H5P_DEFAULT);

  H5Sclose(space_id);
  H5Fclose(file_id);
}

template <typename T, int N_dims>
void Hdf5Save(const std::string& file_path, const std::string& dataset_name,
              const T* data, hsize_t (&start)[N_dims],
              hsize_t (&count)[N_dims]) {
  // disable libhdf5 error printing
  herr_t (*old_func)(hid_t, void*);
  void* old_client_data;

  H5Eget_auto(H5E_DEFAULT, &old_func, &old_client_data);
  H5Eset_auto(H5E_DEFAULT, nullptr, nullptr);

  // attempt to open the file
  hid_t file_id = H5Fopen(file_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
  if (file_id < 0) {
    throw std::invalid_argument(">>>>>>>>>>>>>>>>>>> Could not open " +
                                file_path);
  }

  MPI_Barrier(MPI_COMM_WORLD);

  hid_t dataset_id = H5Dopen1(file_id, dataset_name.c_str());
  if (dataset_id < 0) {
    throw std::invalid_argument(">>>>>>>>>>>>>>>>>> Could not open dataset " +
                                dataset_name);
  }

  hid_t space_id = H5Dget_space(dataset_id);
  if (space_id < 0) {
    throw std::runtime_error(
        "Could not retrieve dataspace extent, file seems to be corrupted");
  }

  H5Sselect_hyperslab(space_id, H5S_SELECT_SET, start, nullptr, count, nullptr);
  hid_t mem_space_id = H5Screate_simple(N_dims, count, nullptr);

  // write the data to disk
  if (H5Dwrite(dataset_id, MPI_Type<T>(), mem_space_id, space_id, H5P_DEFAULT,
               data) < 0) {
    throw std::runtime_error(
        "Failed to write data, target data set is probably of wrong size or "
        "type");
  }

  // clean up the handles
  H5Sclose(mem_space_id);
  H5Sclose(space_id);
  H5Dclose(dataset_id);
  H5Fclose(file_id);

  // restore libhdf5 error printing - everything below is critical, hdf5 should
  // do the diagnostics
  H5Eset_auto(H5E_DEFAULT, old_func, old_client_data);
}

using DataType = double;
constexpr int N_dims = 2;
constexpr int N_point_dims = 13;

hsize_t dims[N_dims];
hsize_t starts[N_dims];
hsize_t counts[N_dims];

template <typename T>
T Str2Num(const std::string& s) {
  return 0;
}

template <>
double Str2Num<double>(const std::string& s) {
  return std::stod(s);
}

template <>
long Str2Num<long>(const std::string& s) {
  return std::stol(s);
}

void process_basedir(std::string input_basedir, string file_path,
                     string dataset_name) {
  /* 同样，打开前先判断 */
  DIR* basedir_open;
  if ((basedir_open = opendir(input_basedir.c_str())) == NULL) {
    cout << "in valid insub dir:" << input_basedir << endl;
    std::abort();
  }

  struct dirent* subdir_p;
  vector<std::string> input_files;
  while ((subdir_p = readdir(basedir_open)) != NULL) {
    if (subdir_p->d_type == DT_REG && subdir_p->d_name != one_dot &&
        subdir_p->d_name != tow_dot) {  // subdir也要单独掕出 . ..

      input_files.push_back(subdir_p->d_name);
    } else if (subdir_p->d_name == one_dot || subdir_p->d_name == tow_dot) {
      //			cout << "omitting . and/or .." << endl;
    } else {
      cout << "error files..." << endl;
      std::abort();
    }
  }

  int n = input_files.size();
  // sort to assure
  sort(input_files.begin(), input_files.end());

  int file_count = n / mpi_size;
  int remainder = n % mpi_size;

  int file_start;
  if (mpi_rank < remainder) {
    file_count += 1;
    file_start = file_count * mpi_rank;
  } else {
    file_start = file_count * mpi_rank + remainder;
  }

  int total_count;
  MPI_Allreduce(&file_count, &total_count, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);
  if (total_count != n) {
    std::exit(1);
  }

  if (file_count == 0) {
    clog << "I do nothing..." << endl;
  }

  vector<DataType> data;

  long point_count = 0;

  for (int i = file_start; i < file_start + file_count; ++i) {
    ifstream ifs;
    const auto input_path = input_basedir + "/" + input_files[i];
    cout << "Input file: " << input_path << endl;

    ifs.open(input_path);
    string line, field;

    while (getline(ifs, line)) {
      point_count++;

      stringstream ss;
      ss << line;

      int num_var = 0;
      while (ss >> field) {
        ++num_var;

        auto point_coord = Str2Num<DataType>(field);
        data.push_back(point_coord);

        std::cout.precision(7);
        if (point_count < 3) {
          cout << point_coord << " ";
        }
      }

      if (point_count == 3) {
        cout << endl;
      }

      if (num_var != N_point_dims) {
        cerr << "Error input file " << input_path << "..." << endl;
        MPI_Abort(MPI_COMM_WORLD, 1);
      }
    }

    ifs.close();
  }

  clog << "P-" << mpi_rank << " has " << point_count << " points..." << endl;

  long all_point_count[mpi_size + 1];
  all_point_count[0] = 0;
  MPI_Allgather(&point_count, 1, MPI_LONG, all_point_count + 1, 1, MPI_LONG,
                MPI_COMM_WORLD);

  for (int i = 1; i < mpi_size + 1; ++i) {
    all_point_count[i] = all_point_count[i - 1] + all_point_count[i];
  }

  dims[0] = all_point_count[mpi_size];
  dims[1] = N_point_dims;

  if (mpi_rank == 0) {
    Hdf5Create<DataType, N_dims>(file_path, dataset_name, dims);
    cout << "Create File " << file_path << " success..." << endl;

    cout << "Create HDF5 dims ";
    for (int i = 0; i < N_dims; ++i) {
      cout << dims[i] << " ";
    }

    cout << "..." << endl;
  }

  MPI_Barrier(MPI_COMM_WORLD);
  starts[0] = all_point_count[mpi_rank];
  starts[1] = 0;

  counts[0] = point_count;
  counts[1] = N_point_dims;

  Hdf5Save<DataType, N_dims>(file_path, dataset_name, data.data(), starts,
                             counts);
}

int main(int argc, char* argv[]) {
  MPI_Init(&argc, &argv);
  MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank);
  MPI_Comm_size(MPI_COMM_WORLD, &mpi_size);

  if (argc < 2) {
    cout << "Enter the base directory..." << endl;
    std::abort();
  }

  /* 输入、输出文件夹 */
  string dataset_path = argv[1];

  int len = dataset_path.size();

  string file_path;
  string dataset_name = "DBSCAN";

  file_path = "result.h5";

  process_basedir(dataset_path, file_path, dataset_name);

  MPI_Finalize();
  return 0;
}
